#include <stdio.h>
#include <stdlib.h>

#include <GL/gl.h>     // The GL Header File
#include <GL/glut.h>   // The GL Utility Toolkit (Glut) Header

#include <time.h>

#include "ODLGameLoop.h"
#include "ODLGameLoop_private.h"
#include "../common/GameEntity.h"
#include "../common/GameLoopConstants.h"

#include "../common/InputManager.h"

ODLGameLoopState odlGameLoopState;

void ODLGameLoop_start(GameEntity** gameEntitiesArr, int gameEntitiesArrLength) {
	ODLGameLoop_initGameLoopState(gameEntitiesArr, gameEntitiesArrLength);
	InputManager_init();
	ODLGameLoop_initOpenGL();
}

/*
 * One input event (at most) is processed during an update state.
 * This is not ideal, as, if many input events are generated during a short period of time,
 * the game will give the impression of lagging behind the user's input	AND/OR ignore some input events (such as holding two keys at the same time)
 */
void ODLGameLoop_updateState() {
	ArrowKeyPressedEvent* inputEvent = InputManager_popEvent();
	int i;
	for(i=0; i < odlGameLoopState.gameEntitiesLength; i++) {
		GameEntity_updateState(odlGameLoopState.gameEntities[i], inputEvent);
	}
}

void ODLGameLoop_onOpenGLIdle() {
	double now = glutGet(GLUT_ELAPSED_TIME);
	double timeElapsedMs = ((now-odlGameLoopState.lastLoopTime)*1000)/(CLOCKS_PER_SEC);
	odlGameLoopState.timeAccumulatedMs += timeElapsedMs;

	while(odlGameLoopState.timeAccumulatedMs >= DESIRED_STATE_UPDATE_DURATION_MS) {
		ODLGameLoop_updateState();
		odlGameLoopState.timeAccumulatedMs -= DESIRED_STATE_UPDATE_DURATION_MS;

		odlGameLoopState.upsCount++;
		ODLGameLoop_updateMeasurements();

		/*
		 * next line will singal OpenGL that it should call the ODLGameLoop_updateGraphics() function.
		 *
		 * From the official OpenGL GLUT docs:
		 *
		 * "Multiple calls to glutPostRedisplay before the next display callback opportunity generates only a single redisplay callback."
		 *
		 * For this particular case this translates to the following:
		 *
		 * - if this loop is executed several times, it will ask Open GL each time to update the graphics (post redisplay)
		 * - all redisplay calls are batched together until the graphics can actually be displayed
		 * - all the batched redisplay calls will only generate on actual graphics update
		 * - this effectively acts as a "skip frame" mechanism when necessary
		 *
		 */
		glutPostRedisplay();
	}

	odlGameLoopState.lastLoopTime = now;
}

void ODLGameLoop_updateGraphics() {
	/*
	 * Crude way to simulate delays in graphics updates.
	 * This will test weather the game loop handles correctly the fact of skipping frames when graphics drawing takes too much time
	 * in order to have an average constant frequency of calls to updateState
	 */
	if(odlGameLoopState.simulatedDelayDurationMs > 0) {
		double now = glutGet(GLUT_ELAPSED_TIME);
		while(((glutGet(GLUT_ELAPSED_TIME)-now)*1000)/(CLOCKS_PER_SEC) < odlGameLoopState.simulatedDelayDurationMs) {

		}
	}

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen
	int i;
	for(i=0; i < odlGameLoopState.gameEntitiesLength; i++) {
		GameEntity_updateGraphics(odlGameLoopState.gameEntities[i]);
	}
	odlGameLoopState.fpsCount++;
	glutSwapBuffers();
}

void ODLGameLoop_updateMeasurements() {
	double now = glutGet(GLUT_ELAPSED_TIME);
	double timeElapsedMs = ((now-odlGameLoopState.lastMeasurementTime)*1000)/(CLOCKS_PER_SEC);
	if(timeElapsedMs>=500) {
		double totalPrecision = 0;
		int i;
		for(i=0; i < odlGameLoopState.gameEntitiesLength; i++) {
			totalPrecision += odlGameLoopState.gameEntities[i]->precision;
		}
		double avgPrecision = totalPrecision/((double)odlGameLoopState.gameEntitiesLength);

		double ups = (odlGameLoopState.upsCount*1000)/timeElapsedMs;
		double fps = (odlGameLoopState.fpsCount*1000)/timeElapsedMs;
		char title[100];
		sprintf(title, "C Game Loop Study - On Demand Game Loop. FPS:%d UPS:%d SimDelay:%d Precision:%.2f", (int)fps, (int)ups, (int)odlGameLoopState.simulatedDelayDurationMs, avgPrecision);
		glutSetWindowTitle(title);
		odlGameLoopState.upsCount = 0;
		odlGameLoopState.fpsCount = 0;
		odlGameLoopState.lastMeasurementTime = now;
	}
}

void ODLGameLoop_initGameLoopState(GameEntity** gameEntities, int gameEntitiesLength) {
	odlGameLoopState.gameEntities = gameEntities;
	odlGameLoopState.gameEntitiesLength = gameEntitiesLength;

	odlGameLoopState.lastLoopTime = glutGet(GLUT_ELAPSED_TIME);
	odlGameLoopState.lastMeasurementTime = glutGet(GLUT_ELAPSED_TIME);

	odlGameLoopState.desiredStateUpdatesPerSecond = 50;
	odlGameLoopState.desiredStateUpdateDurationMs = 20; // obtained as: 1000/desiredStateUpdatesPerSecond

	odlGameLoopState.upsCount = 0;
	odlGameLoopState.fpsCount = 0;

	odlGameLoopState.desiredStateUpdateDurationMs = 0;
}

void ODLGameLoop_initOpenGL() {
	char *my_argv[] = { "dummyArgs", NULL };
	int   my_argc = 1;
	glutInit(&my_argc, my_argv);

	glShadeModel(GL_SMOOTH);							// Enable Smooth Shading
	glClearColor(0.0f, 0.0f, 0.0f, 0.5f);				// Black Background
	glClearDepth(1.0f);									// Depth Buffer Setup
	glEnable(GL_DEPTH_TEST);							// Enables Depth Testing
	glDepthFunc(GL_LEQUAL);								// The Type Of Depth Testing To Do
	glEnable(GL_COLOR_MATERIAL);
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH);
	glutInitWindowSize(VIEW_WIDTH, VIEW_HEIGHT);
	glutInitWindowPosition(WINDOW_POS_X, WINDOW_POS_Y);
	glutCreateWindow("C Game Loop Study - On Demand Looping Game Loop");

	glutDisplayFunc(ODLGameLoop_updateGraphics);
	glutIdleFunc(ODLGameLoop_onOpenGLIdle);
	glutReshapeFunc(ODLGameLoop_onWindowReshape);
	glutKeyboardFunc(ODLGameLoop_onKeyboard);
	glutMainLoop();
}

void ODLGameLoop_onWindowReshape() {
	glMatrixMode(GL_PROJECTION);
	glOrtho(0, VIEW_WIDTH, -VIEW_HEIGHT, 0, 1, 100);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	if (glGetError() != GL_NO_ERROR) {
		printf("Error init-ing OpenGL. \n");
	}
}

void ODLGameLoop_onKeyboard(unsigned char key, int x, int y) {
	switch (key) {
	case 27: {            // ESCAPE key
		ODLGameLoop_free();
		exit(0);
		break;
	}
	case 'a': {
		InputManager_pushEvent('L');
		break;
	}
	case 'd': {
		InputManager_pushEvent('R');
		break;
	}
	case 'w': {
		InputManager_pushEvent('U');
		break;
	}
	case 's': {
		InputManager_pushEvent('D');
		break;
	}
	case 'o': {
		odlGameLoopState.simulatedDelayDurationMs -= 50;
		if(odlGameLoopState.simulatedDelayDurationMs < 0) {
			odlGameLoopState.simulatedDelayDurationMs = 0;
		}
		break;
	}
	case 'p': {
		odlGameLoopState.simulatedDelayDurationMs += 50;
		break;
	}
	}
}

void ODLGameLoop_free() {
	int i;
	for(i=0; i < odlGameLoopState.gameEntitiesLength; i++) {
		GameEntity_free(odlGameLoopState.gameEntities[i]);
	}
	free(odlGameLoopState.gameEntities);
	InputManager_free();
	printf("Freed resources\n");
}
